# -*- coding: utf-8 -*-
"""
Created on Tue Dec 04 16:12:46 2012

@author: Timothy.Wiseman
"""

# -*- coding: utf-8 -*-
"""
Created on Mon Oct 31 13:37:05 2011

@author: Timothy.Wiseman

To Do: 
Make gui
Possible shrinking out from sides intead of the spiral.
Make the 

"""
 
#################################################################
# Imports
#################################################################
import os

from PIL import Image
from functools import partial
#from itertools import cycle
from collections import namedtuple
import random

import multiprocessing



#################################################################
# Constants
#################################################################
rs = Image.ANTIALIAS #Resample type, aliased for conveniance 

#################################################################
#exceptions
#################################################################
class InvalidFileError(Exception): pass

#################################################################
#classes
#################################################################
class PicBox(namedtuple('PicBox', 'le up ri lo')):
    #made to work with PIL, so returns a sizeTuple that works
    #with functions such as Image.resize()
    
    __slots__ = ()
    defaultFrac = 1
    defaultOffset = 0

    @property
    def width(self):
        #PIL users coordate system with 0, 0 in upper lsef
        return abs(self.ri - self.le)
    
    @property
    def height(self):
        #PIL uses coordinate system with 0, 0 in upper left
        return abs(self.lo-self.up)
        
    @property
    def size(self):
        return self.width *self.height
        
    @property
    def sizeTuple(self):
        return (self.width, self.height)
        
    #These functions return quadrants of the box    
    #all division is ineger division
    def ulBox(self):
        """Returns the box occupying the upper 1/9th of the current box"""
        return PicBox(
                    self.le, 
                    self.up, 
                    self.le + self.width//3, 
                    self.up + self.height//3
                    )
                           
    def urBox(self):
        return PicBox(
                self.le + self.width*2//3,
                self.up,
                self.ri,
                self.up + self.height//3)
                
    def lrBox(self):
        return PicBox(
                self.le + self.width*2//3,
                self.up +self.height*2//3,
                self.ri,
                self.lo)
                
    def llBox(self):
        return PicBox(
                self.le,
                self.up +self.height*2//3,
                self.le + self.width//3,
                self.lo)
                
    def centerBox(self):
        """The box occupying the center 1/9th the current box"""
        return PicBox(
                    self.le + self.width//3, #left
                    self.up + self.height//3, #up
                    self.le + self.width*2//3, #right
                    self.up + self.height*2//3, #lower
                    )
                    
    def centeredBoxSized(self, otherSize):
        """Given a size tuple (such as (width, height) or the sizeTuple property
        from a pic box) return a pixBox object of that size centered within
        this picBox."""
        
        leftOffset = (self.width - otherSize[0])//2 #integer division, want the floor
        downOffset = (self.height - otherSize[1])//2 #integer division, wnat the floor
        
        return PicBox(
            self.le + leftOffset, #left
            self.up + downOffset, #up
            self.le + leftOffset + otherSize[0],
            self.up + downOffset + otherSize[1]
            )
                
    #Ok, there is a better way to handle the offset dir, need to refactor all but the last of these
                
    def urCornorBox(self, frac = defaultFrac, offset = defaultOffset, offsetDir = 1):
        """Returns the box coming from the upper right cornor of the current
        box with the specified fraction of the size.  This may become negative
        or it may exceed the size of a given area under consideration.
        
        It is moved down by the percent specified in offset.  To get the 
        exact cornor, use an offset of 0.        
        
        offsetDir 1 is clockwise, offset dir -1 is counterclockwise.
        """
        if offsetDir == 1:
            newup = self.up - int(self.height * frac)  + int(self.height * offset) #upper
            newlo = self.up        + int(self.height * offset) #lower
            newle = self.ri #left
            newri = self.ri + int(self.width * frac) #right
        else:
            newup = self.up - int(self.height * frac) #upper
            newlo = self.up  #lower
            newle = self.ri  - int(self.width * offset)#left
            newri = self.ri + int(self.width * frac)  - int(self.width * offset) #right

        return PicBox(newle, newup, newri, newlo)
                
    def ulCornorBox(self, frac = defaultFrac, offset = defaultOffset, offsetDir = 1):
        """Returns the box coming from the upper left cornor of the current
        box with the specified fraction of the size.  This may become negative
        or it may exceed the size of a given area under consideration.
        
        Shifted right by the offset.  To get the exact cornor, use an offset 
        of 0."""       
        if offsetDir == 1:
            newle = self.le - int(self.width * frac)    + int(self.width*offset) #left  
            newri = self.le       + int(self.width*offset) #right
            newup =  self.up - int(self.height * frac) #upper 
            newlo = self.up #lower
        else:
            newle = self.le - int(self.width * frac) #left
            newri = self.le #right
            newup =  self.up - int(self.height * frac) + int(self.height * offset)#upper 
            newlo = self.up + int(self.height * offset)#lower            
        
        return PicBox(newle, newup, newri, newlo)
        
            
    def llCornorBox(self, frac = defaultFrac, offset = defaultOffset, offsetDir = 1):
        """Returns the box coming from the lower left cornor of the current
        box with the specified fraction of the size.  This may become negative
        or it may exceed the size of a given area under consideration.
        
        Shifted up by the offset.  to get the exact cornor, use an offset of 0."""

        
        if offsetDir == 1:
            newup = self.lo    -int(self.height * offset) #upper
            newlo = self.lo + int(self.height * frac)     -int(self.height * offset) #lower
            newle = self.le- int(self.width * frac) #left
            newri = self.le #right
        else:
            newup = self.lo     #upper
            newlo = self.lo + int(self.height * frac)    #lower   
            newle = self.le- int(self.width * frac)  + int(self.width*offset)#left
            newri = self.le  + int(self.width*offset)#right
        
        return PicBox(newle, newup, newri, newlo)
                
    def lrCornorBox(self, frac = defaultFrac, offset = defaultOffset, offsetDir = 1):
        """Returns the box coming from the lower right cornor of the current
        box with the specified fraction of the size.  This may become negative
        or it may exceed the size of a given area under consideration.
        
        It is moved left by the percent specified in offset.  To get the 
        exact cornor, use an offset of 0"""
        
        if offsetDir == 1:
            newle = self.ri    - offsetDir * int(self.width * offset) #left
            newup = self.lo #upper
            newri = self.ri + int(self.width *frac)     - offsetDir * int(self.width * offset) #right
            newlo = self.lo + int(self.height * frac) #lower
        else:
            newle = self.ri     #left
            newup = self.lo-int(self.height * offset) #upper
            newri = self.ri + int(self.width *frac)     #right
            newlo = self.lo + int(self.height * frac) -int(self.height * offset)#lower
        
        return PicBox(newle, newup, newri, newlo)
                
#################################################################
#Functions                    
#################################################################

def resizeImg(curImg, sizing='selective', sizeTuple=(20, 20)):   
    if isinstance(curImg, dict):
        curImg = Image.fromstring(**curImg) 
        
    if sizing == 'forced':
        pilImg = curImg.resize(sizeTuple, rs)
    elif sizing == 'flexible':
        pilImg = maxResizeBox(curImg, sizeTuple)
    elif sizing == 'selective':
        #this option chooses the sizing to use based on how far off the images
        #are.  If it is more than 5% use flexible, otherwise use forced.
        if (abs(curImg.size[0]-sizeTuple[0])/float(sizeTuple[0]) > .05 or
                abs(curImg.size[1]-sizeTuple[1])/float(sizeTuple[1]) > .05):
            pilImg = curImg.resize(sizeTuple, rs)
        else:
           pilImg = maxResizeBox(curImg, sizeTuple) 
            
    #make it picklable, this is necessary to help it work with the multithreading.
    imgData = {'data': pilImg.tostring(), 'size':pilImg.size, 'mode':pilImg.mode}
    return imgData
      
      
    



def yieldPicSpiral(picFileNames, mainSize = (3200, 2400), 
                 rotate = 'clockwise', frac = .5, offset = 0, border = 15,
                 sizing = 'flexible', maxNumImages = 22):
    """Yields success images in generating the pic spiral.  Useful for
    showing proviews and updating a progress bar"""
    bgColor = (255, 255, 255) #white
    spiral = Image.new('RGB', mainSize, bgColor)
    firstBox = PicBox(0, 0, mainSize[0], mainSize[1])  
    picsUsed = []
    picNumList = []
    picNum = 0 #just initialize it
    boxList = [firstBox.centerBox(), ]
    farLeft = firstBox.centerBox().le   
    random.seed() #set the seed for random from the system clock
    offsetDir = 1 #used only if rotate is alternate 

    firstPic = True #tracks whether or not the first, makes it possible
                    #to treat the first differently
    if len(picFileNames) < maxNumImages:
        maxNumImages = len(picFileNames)

    tempList = []
    partialPrepResize = partial(prepResize, mainSize = mainSize)
    pool= multiprocessing.Pool()
    imgList = pool.map(partialPrepResize, random.sample(picFileNames, maxNumImages))    

   
    while farLeft >= border and len(boxList) > 0:
        assert len(imgList) == maxNumImages
        picNumList = []
        
        if firstPic and sizing == 'flexible':
            #If its the first one, let it adjust the size of the box to
            #avoid the black bars.  Otherwise, use the normal procedure
            #that permits either black bars or some stretching of the img.
            curImg = imgList[0]
            curImg, centerBox = centerResizeBox(curImg, boxList[0])
            boxList = [centerBox, ] 
            #reduce it to a pickable form mostly because all the others are.
            picsToPaste = [{'data': curImg.tostring(), 'size':curImg.size, 'mode':curImg.mode}, ]
            
            firstPic = False
            picsUsed.append(0)
            picNumList.append(0)
        else:
            #generate the pictures to be pasted this time around        
                  
            for box in boxList:
                #Randomly select a picture to use next, but try to minimize 
                #reptition
                if len(picsUsed) >= len(imgList):
                    picsUsed = []
                picNum = random.randrange(maxNumImages)
                while picNum in picsUsed:
                    picNum = random.randrange(maxNumImages)
                picsUsed.append(picNum)  
                picNumList.append(picNum)
            pool = multiprocessing.Pool() 
            #since pool.map only takes one iterable, make a partial function that fixes the other paramaters
            partialResizer = partial(resizeImg, sizing = sizing, sizeTuple =boxList[0].sizeTuple)
            #picsToPaste = map(partialResizer, [imgList[x] for x in picNumList])
            picsToPaste = pool.map(partialResizer, [imgList[x] for x in picNumList])
            pool.close()
            pool.join()
        
        
        for box, curImg, picNum in zip(boxList, picsToPaste, picNumList):
            pilImg = Image.fromstring(**curImg) 
            spiral.paste(pilImg, box)  
            imgList[picNum] = curImg #replace it with the now smaller version.  This should resize faster next time
        
        #now gen the new box list
        tempList = [] #used to hold the boxes for this loop
        for box in boxList:
            if rotate == 'clockwise':                
                tempList.extend([box.urCornorBox(frac, offset, offsetDir=1), 
                                 box.ulCornorBox(frac, offset, offsetDir=1), 
                                 box.lrCornorBox(frac, offset, offsetDir=1), 
                                 box.llCornorBox(frac, offset, offsetDir=1)])
            elif rotate == 'counterclockwise':
                tempList.extend([box.urCornorBox(frac, offset, offsetDir=-1), 
                                 box.ulCornorBox(frac, offset, offsetDir=-1), 
                                 box.lrCornorBox(frac, offset, offsetDir=-1), 
                                 box.llCornorBox(frac, offset, offsetDir=-1)])
            #in practice, these two don't come out that nicely
            #so the option is removed from the gui.
            elif rotate == 'alternate':
                offsetDir = offsetDir * -1
                tempList.extend([box.urCornorBox(frac, offset, offsetDir=offsetDir), 
                                 box.ulCornorBox(frac, offset, offsetDir=offsetDir), 
                                 box.lrCornorBox(frac, offset, offsetDir=offsetDir), 
                                 box.llCornorBox(frac, offset, offsetDir=offsetDir)])
            elif rotate == 'both':
                tempList.extend([box.urCornorBox(frac, offset, offsetDir=1), 
                             box.ulCornorBox(frac, offset, offsetDir=1), 
                             box.lrCornorBox(frac, offset, offsetDir=1), 
                             box.llCornorBox(frac, offset, offsetDir=1)])
                tempList.extend([box.urCornorBox(frac, offset, offsetDir=1), 
                             box.ulCornorBox(frac, offset, offsetDir=1), 
                             box.lrCornorBox(frac, offset, offsetDir=1), 
                             box.llCornorBox(frac, offset, offsetDir=1)])
                
                
            if box.le < farLeft:
                farLeft = box.le

        #get rid of boxes that already have a pic in them.
        boxList = [x for x in tempList if spiral.crop(x).getcolors(1) and spiral.crop(x).getcolors(1)[0][1] == bgColor]
        yield spiral
            
    
    

def genPicSpiral(picFileNames, mainSize = (1600, 1200), 
                 rotate = 'clockwise', frac = .5, offset = 0, border = 15,
                 sizing = 'flexible'):
    """This takes a list of file names and a size tuple, will generate a spiral
    picture based on them.

    It returns the PIL image object.  It accept file names 
    rather than PIL images to avoid loading more into memmory than are needed
    and since they will all needed to be resized for use.

    Defaults to 1600x1200 since that is my common resolution.    
    
    Options for rotation are clockwise, counterclockwise, alternate, and both.
    """
    picIter = yieldPicSpiral(picFileNames, mainSize, 
                 rotate, frac, offset, border,
                 sizing)
    for pic in picIter:
        print 'finished a pass'
    return pic

            
    
def procFileStr(fileStr, extension = '', strict = False):
    """Takes a string which consists of file names and directory names
    separated by newlines.  It then processes each line to see if it is a file
    or a directory.  If it is a directory, it will process the contents of the 
    directory and add any files that match the extension to the file list.
    If it is a file, it will ensure it has the appropriate extension and exists
    and add it to the list.  
    
    If strict is set to false, it will silently remove files that do not exist
    or that do not have the right extension.  If strict is set to true,
    it will raise an error on files or folders 
    that either do not exist or files that have the wrong extension.
    
    To accept all files regardless of extension, pass an extension of a 0 length string."""
     
    
    extension = extension.upper()
    listSplit = str(fileStr).strip().split('\n')
    fileList = []
    for entry in listSplit:
        if os.path.isfile(entry):
            if entry.upper().endswith(extension):
                fileList.append(entry)
            elif strict:
                raise InvalidFileError, 'File {} is invalid'.format(entry)
        elif os.path.isdir(entry):
            for path, dirnames, filenames in os.walk(entry):
                for filename in filenames:
                    if filename.upper().endswith(extension):
                        fileList.append(os.path.join(path, filename))
        elif strict:
            raise InvalidFileError, 'File {} is invalid'.format(entry)
    return fileList
    
def maxResizeBox(img, maxSizeTuple, 
                 bgColor = (0, 0, 0) #black
                 ):
    """
    Takes an image and a maximum size tuple.
    
    Returns an image of exactly the size of the maxSizeTuple, with a background
    of bgColor and the image resized to as close as that size as possible
    while maintaining its original aspect ratio.  

    Will not deform the image.    
    """
    if img.size != maxSizeTuple:
        minRatio = min(
                        float(maxSizeTuple[0])/float(img.size[0]),
                        float(maxSizeTuple[1])/float(img.size[1])
                        )
        #int will automatically take the floor of the value
        sizeTuple = (
                        int(img.size[0] * minRatio),
                        int(img.size[1] * minRatio)
                    )
        insidePicBox = PicBox(
                        (maxSizeTuple[0] - sizeTuple[0])//2, #integer division le
                        (maxSizeTuple[1] - sizeTuple[1])//2, #integer division up
                        (maxSizeTuple[0] - sizeTuple[0])//2 +sizeTuple[0], #ri
                        (maxSizeTuple[1] - sizeTuple[1])//2 +sizeTuple[1] #do
                        )
        newImg = Image.new('RGB', maxSizeTuple, bgColor)
        
        newImg.paste(img.resize(insidePicBox.sizeTuple, rs), 
                     insidePicBox
                     )
        return newImg
    else:
        return img
        
def centerResizeBox(img, tgtPicBox):
    """
    Takes an image and a picBox to fit it in.  It will then return the image
    sized so fits within the PicBox along with a picbox that would center
    the image within the tgtPicbox"""
    if isinstance(img, dict):
        img = Image.fromstring(**img) 
    minRatio = min(float(tgtPicBox.sizeTuple[0])/float(img.size[0]),
                    float(tgtPicBox.sizeTuple[1]/float(img.size[1])))
    sizeTuple = (int(img.size[0] * minRatio),
                 int(img.size[1] * minRatio))
    return img.resize(sizeTuple, rs), tgtPicBox.centeredBoxSized(sizeTuple)
    
def prepResize(imgFn, mainSize):
    """This is meant as a prep resize using a fast algorithm for all pics.
    This supports the two resize setps mentioned at 
    http://united-coders.com/christian-harms/image-resizing-tips-every-coder-should-know/
    """
    img = Image.open(imgFn)
    twoThirdsMainSize = (int(2.0 * mainSize[0]/3.0), int(2.0 * mainSize[1]/3.0))
    if mainSize[0] > img.size[0]:
        minRatio = min(float(mainSize[0])/float(img.size[0]),
                        float(mainSize[1]/float(img.size[1])))
        sizeTuple = (int(img.size[0] * minRatio),
                     int(img.size[1] * minRatio))
        img = img.resize(sizeTuple)
    elif twoThirdsMainSize[0] > img.size[0]:
        minRatio = min(float(twoThirdsMainSize[0])/float(img.size[0]),
                        float(twoThirdsMainSize[1]/float(img.size[1])))
        sizeTuple = (int(img.size[0] * minRatio),
                     int(img.size[1] * minRatio))
        img = img.resize(sizeTuple)
    else: #if its smaller than 2/3s the mainSize, then use the highest option available
        minRatio = min(float(twoThirdsMainSize[0])/float(img.size[0]),
                        float(twoThirdsMainSize[1]/float(img.size[1])))
        sizeTuple = (int(img.size[0] * minRatio),
                     int(img.size[1] * minRatio))
        img = img.resize(sizeTuple, rs)
                     
    return {'data': img.tostring(), 'size':img.size, 'mode':img.mode}
            
    

    
####################################################################

if __name__=='__main__':
    #import cProfile
    picFiles = procFileStr(r'C:\Users\Timothy.Wiseman\Documents\PicSpiral\TestFiles', 'jpg')
            
    #print cProfile.run('genPicSpiral(picFiles)')
    for count in range(5):
        newPic = genPicSpiral(picFiles, mainSize = (3200, 2400), offset = 0.0, rotate = 'counterclockwise')
        newPic.save(r'C:\Users\Timothy.Wiseman\Documents\PicSpiral\TestFiles\Spiral_offset00_' + str(count) + '.jpg')
    #newPic.show()
